断点续传的原理
其实断点续传的原理很简单,就是在 Http 的请求上和一般的下载有所不同而已。
打个比方,浏览器请求服务器上的一个文时,所发出的请求如下:
假设服务器域名为 www.sjtu.edu.cn,文件名为 down.zip。
1 2 3 4 5 6 7
| GET /down.zip HTTP/1.1 Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/vnd.ms- excel, application/msword, application/vnd.ms-powerpoint, */* Accept-Language: zh-cn Accept-Encoding: gzip, deflate User-Agent: Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0) Connection: Keep-Alive
|
服务器收到请求后,按要求寻找请求的文件,提取文件的信息,然后返回给浏览器,返回信息如下:
1 2 3 4 5 6 7 8
| 200 Content-Length=106786028 Accept-Ranges=bytes Date=Mon, 30 Apr 2001 12:56:11 GMT ETag=W/"02ca57e173c11:95b" Content-Type=application/octet-stream Server=Microsoft-IIS/5.0 Last-Modified=Mon, 30 Apr 2001 12:56:11 GMT
|
所谓断点续传,也就是要从文件已经下载的地方开始继续下载。所以在客户端浏览器传给 Web 服务器的时候要多加一条信息 – 从哪里开始。
下面是用自己编的一个”浏览器”来传递请求信息给 Web 服务器,要求从 2000070 字节开始。
1 2 3 4
| GET /down.zip HTTP/1.0 User-Agent: NetFox RANGE: bytes=2000070- Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
|
仔细看一下就会发现多了一行 RANGE: bytes=2000070-
这一行的意思就是告诉服务器 down.zip 这个文件从 2000070 字节开始传,前面的字节不用传了。
服务器收到这个请求以后,返回的信息如下:
1 2 3 4 5 6 7 8
| 206 Content-Length=106786028 Content-Range=bytes 2000070-106786027/106786028 Date=Mon, 30 Apr 2001 12:55:20 GMT ETag=W/"02ca57e173c11:95b" Content-Type=application/octet-stream Server=Microsoft-IIS/5.0 Last-Modified=Mon, 30 Apr 2001 12:55:20 GMT
|
和前面服务器返回的信息比较一下,就会发现增加了一行: Content-Range=bytes 2000070-106786027/106786028
,返回的代码也改为 206 了,而不再是 200 了。
知道了以上原理,就可以进行断点续传的编程了。
Java 实现断点续传的关键几点
用什么方法实现提交RANGE: bytes=2000070-
当然用最原始的 Socket 是肯定能完成的,不过那样太费事了,其实 Java 的 net 包中提供了这种功能。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12
| URL url = new URL("http://www.sjtu.edu.cn/down.zip");
HttpURLConnection httpConnection = (HttpURLConnection)url.openConnection();
httpConnection.setRequestProperty("User-Agent","NetFox");
httpConnection.setRequestProperty("RANGE","bytes=2000070");
InputStream input = httpConnection.getInputStream();
|
从输入流中取出的字节流就是 down.zip 文件从 2000070 开始的字节流。 大家看,其实断点续传用 Java 实现起来还是很简单的吧。 接下来要做的事就是怎么保存获得的流到文件中去了。
- 保存文件采用的方法。
我采用的是 IO 包中的 RandAccessFile 类。
操作相当简单,假设从 2000070 处开始保存文件,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12
| RandomAccess oSavedFile = new RandomAccessFile("down.zip","rw"); long nPos = 2000070;
oSavedFile.seek(nPos); byte[] b = new byte[1024]; int nRead;
while((nRead=input.read(b,0,1024)) > 0) { oSavedFile.write(b,0,nRead); }
|
怎么样,也很简单吧。 接下来要做的就是整合成一个完整的程序了。包括一系列的线程控制等等。
断点续传内核的实现
主要用了 6 个类,包括一个测试类。
SiteFileFetch.java 负责整个文件的抓取,控制内部线程 (FileSplitterFetch 类 )。
FileSplitterFetch.java 负责部分文件的抓取。
FileAccess.java 负责文件的存储。
SiteInfoBean.java 要抓取的文件的信息,如文件保存的目录,名字,抓取文件的 URL 等。
Utility.java 工具类,放一些简单的方法。
TestMethod.java 测试类。
我们来分别看看代码:
- SiteFileFetch.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167
|
package NetFox; import java.io.*; import java.net.*; public class SiteFileFetch extends Thread { SiteInfoBean siteInfoBean = null; long[] nStartPos; long[] nEndPos; FileSplitterFetch[] fileSplitterFetch; long nFileLength; boolean bFirst = true; boolean bStop = false; File tmpFile; DataOutputStream output; public SiteFileFetch(SiteInfoBean bean) throws IOException { siteInfoBean = bean; tmpFile = new File(bean.getSFilePath()+File.separator + bean.getSFileName()+".info"); if(tmpFile.exists ()) { bFirst = false; read_nPos(); } else { nStartPos = new long[bean.getNSplitter()]; nEndPos = new long[bean.getNSplitter()]; } } public void run() { try{ if(bFirst){ nFileLength = getFileSize(); if(nFileLength == -1){ System.err.println("File Length is not known!"); }else if(nFileLength == -2){ System.err.println("File is not access!"); }else{ for(int i=0;i<nStartPos.length;i++){ nStartPos[i] = (long)(i*(nFileLength/nStartPos.length)); } for(int i=0;i<nEndPos.length-1;i++){ nEndPos[i] = nStartPos[i+1]; } nEndPos[nEndPos.length-1] = nFileLength; } } fileSplitterFetch = new FileSplitterFetch[nStartPos.length]; for(int i=0;i<nStartPos.length;i++){ fileSplitterFetch[i] = new FileSplitterFetch(siteInfoBean.getSSiteURL(), siteInfoBean.getSFilePath() + File.separator + siteInfoBean.getSFileName(), nStartPos[i],nEndPos[i],i); Utility.log("Thread " + i + " , nStartPos = " + nStartPos[i] + ", nEndPos = " + nEndPos[i]); fileSplitterFetch[i].start(); } siteInfoBean.getSFilePath() + File.separator + siteInfoBean.getSFileName(),nPos[nPos.length-1],nFileLength,nPos.length-1); boolean breakWhile = false; while(!bStop){ write_nPos(); Utility.sleep(500); breakWhile = true; for(int i=0;i<nStartPos.length;i++){ if(!fileSplitterFetch[i].bDownOver){ breakWhile = false; break; } } if(breakWhile) break; } System.err.println("文件下载结束!"); }catch(Exception e){ e.printStackTrace (); } } public long getFileSize(){ int nFileLength = -1; try{ URL url = new URL(siteInfoBean.getSSiteURL()); HttpURLConnection httpConnection = (HttpURLConnection)url.openConnection (); httpConnection.setRequestProperty("User-Agent","NetFox"); int responseCode=httpConnection.getResponseCode(); if(responseCode>=400){ processErrorCode(responseCode); return -2; } String sHeader; for(int i=1;;i++){ sHeader=httpConnection.getHeaderFieldKey(i); if(sHeader!=null){ if(sHeader.equals("Content-Length")){ nFileLength = Integer.parseInt(httpConnection.getHeaderField(sHeader)); break; } } else break; } } catch(IOException e){e.printStackTrace ();} catch(Exception e){e.printStackTrace ();} Utility.log(nFileLength); return nFileLength; } private void write_nPos(){ try{ output = new DataOutputStream(new FileOutputStream(tmpFile)); output.writeInt(nStartPos.length); for(int i=0;i<nStartPos.length;i++){ output.writeLong(fileSplitterFetch[i].nStartPos); output.writeLong(fileSplitterFetch[i].nEndPos); } output.close(); } catch(IOException e){e.printStackTrace ();} catch(Exception e){e.printStackTrace ();} } private void read_nPos(){ try{ DataInputStream input = new DataInputStream(new FileInputStream(tmpFile)); int nCount = input.readInt(); nStartPos = new long[nCount]; nEndPos = new long[nCount]; for(int i=0;i<nStartPos.length;i++){ nStartPos[i] = input.readLong(); nEndPos[i] = input.readLong(); } input.close(); } catch(IOException e){e.printStackTrace ();} catch(Exception e){e.printStackTrace ();} } private void processErrorCode(int nErrorCode){ System.err.println("Error Code : " + nErrorCode); } public void siteStop(){ bStop = true; for(int i=0;i<nStartPos.length;i++) fileSplitterFetch[i].splitterStop(); } }
|
- FileSplitterFetch.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
|
package NetFox; import java.io.*; import java.net.*; public class FileSplitterFetch extends Thread { String sURL; long nStartPos; long nEndPos; int nThreadID; boolean bDownOver = false; boolean bStop = false; FileAccessI fileAccessI = null; public FileSplitterFetch(String sURL,String sName,long nStart,long nEnd,int id) throws IOException { this.sURL = sURL; this.nStartPos = nStart; this.nEndPos = nEnd; nThreadID = id; fileAccessI = new FileAccessI(sName,nStartPos); } public void run(){ while(nStartPos < nEndPos && !bStop){ try{ URL url = new URL(sURL); HttpURLConnection httpConnection = (HttpURLConnection)url.openConnection (); httpConnection.setRequestProperty("User-Agent","NetFox"); String sProperty = "bytes="+nStartPos+"-"; httpConnection.setRequestProperty("RANGE",sProperty); Utility.log(sProperty); InputStream input = httpConnection.getInputStream(); byte[] b = new byte[1024]; int nRead; while((nRead=input.read(b,0,1024)) > 0 && nStartPos < nEndPos && !bStop) { nStartPos += fileAccessI.write(b,0,nRead); } Utility.log("Thread " + nThreadID + " is over!"); bDownOver = true; } catch(Exception e){e.printStackTrace ();} } } public void logResponseHead(HttpURLConnection con) { for(int i=1;;i++){ String header=con.getHeaderFieldKey(i); if(header!=null) Utility.log(header+" : "+con.getHeaderField(header)); else break; } } public void splitterStop(){ bStop = true; } }
|
- FileAccess.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
package NetFox; import java.io.*; public class FileAccessI implements Serializable{ RandomAccessFile oSavedFile; long nPos; public FileAccessI() throws IOException{ this("",0); } public FileAccessI(String sName,long nPos) throws IOException{ oSavedFile = new RandomAccessFile(sName,"rw"); this.nPos = nPos; oSavedFile.seek(nPos); } public synchronized int write(byte[] b,int nStart,int nLen){ int n = -1; try{ oSavedFile.write(b,nStart,nLen); n = nLen; }catch(IOException e){ e.printStackTrace (); } return n; } }
|
- SiteInfoBean.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
|
package NetFox; public class SiteInfoBean { private String sSiteURL; private String sFilePath; private String sFileName; private int nSplitter; public SiteInfoBean() { this("","","",5); } public SiteInfoBean(String sURL,String sPath,String sName,int nSpiltter) { sSiteURL= sURL; sFilePath = sPath; sFileName = sName; this.nSplitter = nSpiltter; } public String getSSiteURL() { return sSiteURL; } public void setSSiteURL(String value) { sSiteURL = value; } public String getSFilePath() { return sFilePath; } public void setSFilePath(String value) { sFilePath = value; } public String getSFileName() { return sFileName; } public void setSFileName(String value) { sFileName = value; } public int getNSplitter() { return nSplitter; } public void setNSplitter(int nCount) { nSplitter = nCount; } }
|
- Utility.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
package NetFox; public class Utility { public Utility() {}
public static void sleep(int nSecond) { try{ Thread.sleep(nSecond); } catch(Exception e) { e.printStackTrace (); } } public static void log(String sMsg) { System.err.println(sMsg); } public static void log(int sMsg) { System.err.println(sMsg); } }
|
- TestMethod.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
package NetFox; public class TestMethod { public TestMethod() { try{ SiteInfoBean bean = new SiteInfoBean("http://localhost/xx/weblogic60b2_win.exe", "L:\\temp","weblogic60b2_win.exe",5); "weblogic60b2_win.exe",5); SiteFileFetch fileFetch = new SiteFileFetch(bean); fileFetch.start(); } catch(Exception e) { e.printStackTrace (); } }
public static void main(String[] args) { new TestMethod(); } }
|
参考资料